#include "extension_manager.hpp"
#include <cassert>
#include <filesystem>
#include <functional>
#include <memory>
#include <string>
#include <unordered_map>
#include <vector>
#include "app/app_base.hpp"
#include "extension/script_base.hpp"
#include "extension/utility.hpp"
#include "scripting/scripting_api.h"
#include "scripting/scripting_engine.h"
#include "scripting/scripting_module.h"
#include "utils/fs_watcher.hpp"
#include "utils/log.hpp"
#include "utils/path_utils.hpp"

namespace fs = std::filesystem;

struct ExtensionManagerData {
    std::unordered_map<IEditorScript::EditorTarget, std::unordered_set<ScriptPtr *>> editor_extensions;
    std::unordered_map<size_t, ScriptPtr *>                                          function_extensions;
    std::unordered_map<std::string, ScriptPtr>                                       path_to_extension_map;

    FileSystemWatcher fs_watcher;

    void ClearAll ()
    {
        editor_extensions.clear ();
        function_extensions.clear ();
        path_to_extension_map.clear ();
    }

    void ReloadScript (const fs::path &path)
    {
        if (path.extension ().string () != ".ext")
            return;
        std::string str_path = path.string ();
        if (auto it = path_to_extension_map.find (str_path); it != path_to_extension_map.end ())
        {
            if (it->second->reloadable ())
            {
                L_INFO ("post reload script %s(%s)", it->second->title (), str_path.c_str ());
                App->Post ([this, str_path, it] {
                    L_INFO("reloading script %s(%s)", it->second->title (), str_path.c_str ());
                    auto ptr = LoadScriptFile (str_path, &it->second);
                    InsertOrReplaceScript (std::move (ptr), str_path);
                });
            }
        }
    }

    static void FsWatchCb (FileSystemChangedReason r, const char **fn, void *user)
    {
        ExtensionManagerData *data = (ExtensionManagerData *) user;
        switch (r)
        {
        case FileSystemChangedReason::eModified: {
            L_DEBUG ("file changed: %s", fn[0]);
            data->ReloadScript (fn[0]);
        }
        break;
        case FileSystemChangedReason::eCreated: {
            L_DEBUG ("file created: %s", fn[0]);
        }
        break;
        case FileSystemChangedReason::eDeleted: {
            L_DEBUG ("file deleted: %s", fn[0]);
        }
        break;
        case FileSystemChangedReason::eRenamed: {
            L_DEBUG ("file renamed: %s -> %s", fn[0], fn[1]);
        }
        break;
        }
    }

    static auto GetFunctionKey (const std::string &path)
    {
        std::hash<std::string> hash;
        return hash (path);
    }

    ScriptPtr LoadScriptFile (const std::string &path, ScriptPtr *store = nullptr);

    void InsertOrReplaceScript (ScriptPtr script, const std::string &path);

    void InsertScript (ScriptPtr script, const std::string &path);
};

ExtensionManager::ExtensionManager () : d {new ExtensionManagerData}
{
    auto path = "extensions"_AP;
    L_INFO ("add directory watch for %s, ret %d", path.c_str (), (int) d->fs_watcher.AddWatchFolder (path.c_str ()));

    d->fs_watcher.SetCallback (FileSystemChangedCallback {.user = d, .callback = &ExtensionManagerData::FsWatchCb});
    d->fs_watcher.StartWatch ();
}

ExtensionManager::~ExtensionManager ()
{
    delete d;
}

ExtensionManager &ExtensionManager::singleton ()
{
    static ExtensionManager inst;
    return inst;
}

void ExtensionManager::LoadExtensions () const
{
    auto root = "extensions"_AP;
    try
    {
        for (const auto &x: fs::directory_iterator (root))
        {
            if (x.is_regular_file () && x.path ().extension ().string () == ".ext")
            {
                std::string path = x.path ().string ();
                auto        ptr  = d->LoadScriptFile (path);
                d->InsertOrReplaceScript (std::move (ptr), path);
            }
        }
    }
    catch (...)
    {
    }
}

void ExtensionManager::FreeExtensions () const
{
    d->ClearAll ();
    d->fs_watcher.StopWatch ();
}

const std::unordered_set<ScriptPtr *> &ExtensionManager::getEditorExtension (IEditorScript::EditorTarget target) const
{
    assert (target < IEditorScript::eCount);
    return d->editor_extensions[target];
}

const std::unordered_map<size_t, ScriptPtr *> &ExtensionManager::getFunctionExtensions () const
{
    return d->function_extensions;
}

ScriptPtr ExtensionManagerData::LoadScriptFile (const std::string &path, ScriptPtr *store)
{
    ScriptPtr result;

    double const file_time = GetFileTime (path);
    if (store && *store && file_time - (*store)->time () < 3.0)
    {
        // reload branch
        return result;  // skip load
    }

    L_DEBUG ("load script %s", path.c_str ());
    auto content = AnalysisScriptFileContent (path);
    if (content.contains ("language") && content.contains ("code"))  // required
    {
        std::string lang   = content.at ("language");
        std::string code   = content.at ("code");
        auto        engine = GetEngine (lang.c_str ());
        if (!engine)
        {
            L_WARN ("scripting missing lang %s support! ignored.", lang.c_str ());
        }
        else
        {
            std::string title = GetFileName (path);
            if (content.contains ("title"))
                title = content.at ("title");  // use script defined title

            // detect script types
            std::string usage       = "function";
            std::string usage_extra = "N/A";
            if (content.contains ("target"))
            {
                usage_extra = content.at ("target");
                usage       = "editor";
            }

            // build script object
            const ScriptBuildInfo sbi {title.c_str (), path.c_str (), usage.c_str (), usage_extra.c_str (), code.c_str ()};
            IScript              *s = engine->build_script (&sbi);

            if (s)
            {
                if (usage == "function")
                {
                    if (auto fs = s->as<IFunctionalScript> ())
                    {
                        result = std::make_shared<ScriptExtension> (s, file_time);
                        // if (!store)
                        // {
                        //     auto ret = path_to_extension_map.emplace (path, p);
                        //     if (ret.second)
                        //         function_extensions.emplace (std::hash<std::string> {}(path), &ret.first->second);
                        // }
                        // else
                        // {
                        //     *store = p;
                        // }
                    }
                    else
                    {
                        L_WARN ("wrong script type, got: %d, expect: %d", s->usage (), ScriptUsage::eFunction);
                        delete s;
                    }
                }
                else
                {
                    if (auto es = s->as<IEditorScript> ())
                    {
                        result = std::make_shared<ScriptExtension> (s, file_time);
                        // if (!store)
                        // {
                        //     auto ret = path_to_extension_map.emplace (path, p);
                        //     if (ret.second)
                        //         editor_extensions[es->target ()].emplace (&ret.first->second);
                        // }
                        // else
                        // {
                        //     *store = p;
                        // }
                    }
                    else
                    {
                        L_WARN ("wrong script type, got: %d, expect: %d", s->usage (), ScriptUsage::eEditor);
                        delete s;
                    }
                }
            }
            else
            {
                L_WARN ("script %s load failed!", path.c_str ());
            }
        }
    }
    else
    {
        L_WARN ("file %s missing language or code section(s)!", path.c_str ());
    }
    return result;
}

void ExtensionManagerData::InsertOrReplaceScript (ScriptPtr ptr, const std::string &path)
{
    if (!ptr)
        return;

    // find exists script
    auto const it = path_to_extension_map.find (path);
    if (it == path_to_extension_map.end ())
    {
        InsertScript(std::move(ptr), path);
    }
    else
    {
        // found, replace it
        ScriptPtr *old = &it->second;

        auto old_usage = (*old)->usage ();
        auto new_usage = ptr->usage ();

        if (old_usage == new_usage)
        {
            // just replace old script
            if (old_usage == ScriptUsage::eEditor) {
                // editor: switch target
                auto es1 = (*old)->script_->as<IEditorScript>();
                auto es2 = ptr->script_->as<IEditorScript>();

                editor_extensions[es1->target()].erase(old);
                editor_extensions[es2->target()].emplace(old);
            }
            (*old) = std::move (ptr);
        }
        else
        {
            // 1. remove from old container
            switch (old_usage)
            {
            case ScriptUsage::eFunction: {
                function_extensions.erase (GetFunctionKey (path));
            }
            break;
            case ScriptUsage::eEditor: {
                auto es = (*old)->script_->as<IEditorScript> ();
                editor_extensions[es->target ()].erase (old);
            }
            break;
            default: break;
            }

            // 2. insert to new container
            InsertScript(std::move(ptr), path);
        }
    }
}

void ExtensionManagerData::InsertScript (ScriptPtr ptr, const std::string &path)
{
    // not found, insert
    auto insert_ret = path_to_extension_map.emplace (path, std::move (ptr));
    if (insert_ret.second)
    {
        switch (insert_ret.first->second->usage ())
        {
        case ScriptUsage::eFunction: {
            function_extensions.emplace (GetFunctionKey (path), &insert_ret.first->second);
        }
        break;
        case ScriptUsage::eEditor: {
            IScript *underly_script = insert_ret.first->second->script_;
            auto     es             = underly_script->as<IEditorScript> ();
            editor_extensions[es->target ()].emplace (&insert_ret.first->second);
        }
        break;
        default: {
            path_to_extension_map.erase (insert_ret.first);
            L_WARN ("Unknown usage ignored!");
        }
        break;
        }
    }
    else
    {
        L_WARN ("insert script %s failed!", path.c_str ());
    }
}
